// Copyright 2024 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { PrpcClient } from '@/generic_libs/tools/prpc_client';
import { RestGitilesClientImpl } from '@/gitiles/api/rest_gitiles_client';
import {
  ArchiveRequest,
  ArchiveResponse,
  DeepPartial,
  DownloadDiffRequest,
  DownloadDiffResponse,
  DownloadFileRequest,
  DownloadFileResponse,
  GetProjectRequest,
  Gitiles,
  ListFilesRequest,
  ListFilesResponse,
  LogRequest,
  LogResponse,
  Project,
  ProjectsRequest,
  ProjectsResponse,
  RefsRequest,
  RefsResponse,
} from '@/proto/go.chromium.org/luci/common/proto/gitiles/gitiles.pb';
import { MiloInternalClientImpl } from '@/proto/go.chromium.org/luci/milo/proto/v1/rpc.pb';
import {
  QueryCommitHashRequest,
  SourceIndexClientImpl,
} from '@/proto/go.chromium.org/luci/source_index/proto/v1/source_index.pb';

import { PositionHashMapper } from './position_hash_mapper';

/**
 * The same as `gitiles.LogRequest` but also support querying by commit
 * position.
 */
export type ExtendedLogRequest = LogRequest & {
  /**
   * The name of position defined in value of git-footer git-svn-id or
   * Cr-Commit-Position (e.g. refs/heads/master,
   * svn://svn.chromium.org/chrome/trunk/src)
   *
   * Required when `committish` is not specified.
   * Ignored when `committish` is specified.
   */
  readonly ref: string;
  /**
   * The sequential identifier of commit in given branch (`ref`).
   *
   * Required when `committish` is not specified.
   * Ignored when `committish` is specified.
   */
  readonly position: string;
};

function createBaseLogRequest(): ExtendedLogRequest {
  return {
    project: '',
    ref: '',
    position: '',
    committish: '',
    excludeAncestorsOf: '',
    treeDiff: false,
    path: '',
    pageToken: '',
    pageSize: 0,
  };
}

export const ExtendedLogRequest = {
  fromPartial(object: DeepPartial<ExtendedLogRequest>): ExtendedLogRequest {
    // Use the same type declaration as the type generated by ts-proto.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const message = createBaseLogRequest() as any;
    message.project = object.project ?? '';
    message.committish = object.committish ?? '';
    message.ref = object.ref ?? '';
    message.position = object.position ?? '';
    message.excludeAncestorsOf = object.excludeAncestorsOf ?? '';
    message.treeDiff = object.treeDiff ?? false;
    message.path = object.path ?? '';
    message.pageToken = object.pageToken ?? '';
    message.pageSize = object.pageSize ?? 0;
    return message;
  },
};

export const ExtendedGitilesServiceName = 'extended.gitiles.Gitiles';

export interface FusedGitilesClientOpts {
  readonly service?: string;
  /**
   * The host of a LUCI Source Index service. This is used when querying commits
   * by commit position. This does not need to be included in the (react-query)
   * cache key. Given a gitiles host, project, ref and commit position, the
   * resolved commit should be the same regardless which LUCI Source Index
   * service resolves it.
   */
  readonly sourceIndexHost: string;
  /**
   * Whether the milo Gitiles proxy should be used to proxy gitiles requests.
   * Setting to to true slows down the RPC but allows LUCI UI to query more
   * gitiles host.
   *
   * Must be true if the gitiles host is not configured to allow CORS request
   * from LUCI UI.
   * Should be false if the gitiles host is configured to allow CORS request.
   *
   * The requests are always authenticated using the user's credential
   * regardless of this setting.
   */
  readonly useMiloGitilesProxy: boolean;
}

/**
 * Similar to `GitilesClientImpl` but fuses multiple services together to
 * address the limitations of an ordinary `GitilesClientImpl`.
 */
export class FusedGitilesClientImpl implements Gitiles {
  static readonly DEFAULT_SERVICE = ExtendedGitilesServiceName;
  readonly service: string;

  private readonly gitilesClient: RestGitilesClientImpl | undefined;
  private readonly miloClient: MiloInternalClientImpl | undefined;
  private readonly sourceIndexClient: SourceIndexClientImpl;

  private readonly positionHashMappers = new Map<string, PositionHashMapper>();

  constructor(
    private readonly rpc: PrpcClient,
    opts: FusedGitilesClientOpts,
  ) {
    this.service = opts.service || ExtendedGitilesServiceName;
    if (opts.useMiloGitilesProxy) {
      this.miloClient = new MiloInternalClientImpl(
        new PrpcClient({
          host: SETTINGS.milo.host,
          getAuthToken: rpc.getAuthToken,
        }),
      );
    } else {
      this.gitilesClient = new RestGitilesClientImpl(
        new PrpcClient({
          host: rpc.host,
          getAuthToken: rpc.getAuthToken,
        }),
      );
    }
    this.sourceIndexClient = new SourceIndexClientImpl(
      new PrpcClient({
        host: opts.sourceIndexHost,
        getAuthToken: rpc.getAuthToken,
      }),
    );

    this.Log = this.Log.bind(this);
    this.ExtendedLog = this.ExtendedLog.bind(this);
    this.Refs = this.Refs.bind(this);
    this.Archive = this.Archive.bind(this);
    this.DownloadFile = this.DownloadFile.bind(this);
    this.DownloadDiff = this.DownloadDiff.bind(this);
    this.GetProject = this.GetProject.bind(this);
    this.Projects = this.Projects.bind(this);
    this.ListFiles = this.ListFiles.bind(this);
  }

  /**
   * Similar to `GitilesClientImpl.prototype.Log` but it proxy the request
   * through `MiloInternal.ProxyGitilesLog` if the gitiles host does not allow
   * CORS requests from LUCI UI.
   */
  async Log(request: LogRequest): Promise<LogResponse> {
    if (this.gitilesClient) {
      return this.gitilesClient.Log(request);
    }
    return this.miloClient!.ProxyGitilesLog({
      host: this.rpc.host,
      request,
    });
  }

  /**
   * Similar to `GitilesClientImpl.prototype.Log` but
   * 1. support querying by commit position, and
   * 2. does not require the gitiles host to allow CORS requests from LUCI UI.
   */
  async ExtendedLog(request: ExtendedLogRequest): Promise<LogResponse> {
    if (request.committish) {
      // Use `LogRequest.fromPartial` to strip away additional properties in a
      // `ExtendedLogRequest`.
      return this.Log(LogRequest.fromPartial(request));
    }

    // Use a PositionHashMapper to resolve the commit position to a commitish.
    // This
    // * avoids sending a query to Source Index in most cases, which saves
    //   ~300ms query latency, and
    // * allows the query to work even when the ref is renamed as long as a
    //   commit position after the rename is queried first.
    const key = `${request.project} ${request.ref}`;
    let posHashMapper = this.positionHashMappers.get(key);
    if (!posHashMapper) {
      posHashMapper = new PositionHashMapper((pos) =>
        this.sourceIndexClient
          .QueryCommitHash(
            QueryCommitHashRequest.fromPartial({
              host: this.rpc.host,
              repository: request.project,
              positionRef: request.ref,
              positionNumber: pos.toString(),
            }),
          )
          .then((res) => res.hash),
      );
      this.positionHashMappers.set(key, posHashMapper);
    }

    return this.Log(
      // Use `LogRequest.fromPartial` to strip away additional properties in a
      // `ExtendedLogRequest`.
      LogRequest.fromPartial({
        ...request,
        committish: await posHashMapper.getCommitish(
          parseInt(request.position),
        ),
      }),
    );
  }

  Refs(_request: RefsRequest): Promise<RefsResponse> {
    throw new Error('Method not implemented.');
  }
  Archive(_request: ArchiveRequest): Promise<ArchiveResponse> {
    throw new Error('Method not implemented.');
  }
  DownloadFile(_request: DownloadFileRequest): Promise<DownloadFileResponse> {
    throw new Error('Method not implemented.');
  }
  DownloadDiff(_request: DownloadDiffRequest): Promise<DownloadDiffResponse> {
    throw new Error('Method not implemented.');
  }
  GetProject(_request: GetProjectRequest): Promise<Project> {
    throw new Error('Method not implemented.');
  }
  Projects(_request: ProjectsRequest): Promise<ProjectsResponse> {
    throw new Error('Method not implemented.');
  }
  ListFiles(_request: ListFilesRequest): Promise<ListFilesResponse> {
    throw new Error('Method not implemented.');
  }
}
